dispatch 的改造
实现记录日志
前面我们已经简单实现了 dispatch。现在让我们试试,能不能在每次派发 action 的时候,通过 console.log 来打印出相关信息,方便我们在控制台看到 state 的每一步变更呢?事实上,这正是 redux-logger 中间件所做的事情。
dispatch 的本质就是用来触发 reducer 里的执行逻辑。如果我们想要获得 state 的变化信息,就需要在调用 dispatch 的前后插入 console.log,就像这样:1
2
3
4
5
6
7console.log(action + "will dispatch");
console.log(store.getState());
store.dispatch(action);
console.log(action + "already dispatched");
console.log(store.getState());
当然,我们不可能到处都去加上这样的代码。更加优雅的做法是,扩展 dispatch。大致步骤如下:
创建一个名为
addLoggingToDispatch的函数,取代默认的dispatch方法。这个函数会拦截 store 中的 dispatch,并命名为 rawDispatch。它最终会被赋值给 store.dispatch,所以需要返回一个函数,并模拟 rawDispatch 的行为:1
2
3
4
5
6
7const addLoggingToDispatch = store => {
const rawDispatch = store.dispatch;
// 返回的函数就是添加更新日志之后的全新 dispatch
return action => {
// ...
}
}在返回的函数中,我们当然需要调用真实的 rawDispatch 方法,还原它的行为,同时又进行日志记录:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const addLoggingToDispatch = store => {
const rawDispatch = store.dispatch;
// 返回的函数就是添加更新日志之后的全新 dispatch
return action => {
// 按照 action 的类型进行分组
console.group(action.type);
// 输出更新前的 state
console.log("previous state", store.getState());
// 输出当前的 action
console.log("action", action);
// 调用默认的 dispatch 并记录返回值
const returnValue = rawDispatch(action);
// 输出更新后的 state
console.log("next State", store.getState());
// 结束分组
console.groupEnd(action.type);
return returnValue;
}
}美化 和 兼容 当然是必不可少的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27const addLoggingToDispatch = store => {
const rawDispatch = store.dispatch;
if (!console.group) {
return rawDispatch;
}
// 返回的函数就是添加更新日志之后的全新 dispatch
return action => {
// 按照 action 的类型进行分组
console.group(action.type);
// 使用灰色输出更新前的 state
console.log("%c previous state", "color: grey", store.getState());
// 使用蓝色输出当前的 action
console.log("%c action", "color: blue", action);
// 调用默认的 dispatch 并记录返回值
const returnValue = rawDispatch(action);
// 使用绿色输出更新后的 state
console.log("%c next State", "color: green", store.getState());
// 结束分组
console.groupEnd(action.type);
return returnValue;
}
}最后,再加上环境判断:
1
2
3if (process.env.NODE_ENV !== "production") {
store.dispatch = addLoggingToDispatch(store);
}
识别 Promise
我们有时候会遇到需要派发异步 action 的场景,如果 dispatch 能够接收一个 Promise 对象,就能处理 Redux 架构下的异步问题。
1 | const addPromiseSupportToDispatch = store => { |
我们这里通过检查 action 否有个 then 的函数方法来判断接收到的 action 是否是一个 Promise 对象。如果是,那么等待它 resolve 的时候,接收到作为 resolve 的 action 对象,并使用原始的 dispatch 对其进行派发。如果接收的 action 并不是 Promise,那么就直接用 rawDispatch(action) 操作。
现在,既然有了 2 个增强 dispatch 的方法,我们就都来使用:1
2store.dispatch = addLoggingToDispatch(store);
store.dispatch = addPromiseSupportToDispatch(store);
注意,这里的顺序不能换。如果先使用了 addPromiseSupportToDispatch,那么当接收到 Promise 对象的 action 时,日志处理那边就不能正常解析了。
为了提升开发效率,在实际开发中我们可能需要经常改写 dispatch。那么 Redux 是如何协调这么多 dispatch 的改写呢?这就涉及到中间件及中间件串联的知识了。其实每一次对 dispatch 的改写,都可以被封装成一个独立的中间件。
糅合多种 dispatch
前面我们介绍了 dispatch,并提供了 2 种增强功能的例子,它们就是 redux-logger 和 redux-thunk 这两个著名中间件的雏形,同时也奠定了理解 Redux 中间件的基础。
下面汇总之前的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29const addLoggingToDispatch = store => {
const rawDispatch = store.dispatch;
if (!console.group) {
return rawDispatch;
}
return action => {
console.group(action.type);
console.log("%c previous state", "color: grey", store.getState());
console.log("%c action", "color: blue", action);
const returnValue = rawDispatch(action);
console.log("%c next State", "color: green", store.getState());
console.groupEnd(action.type);
return returnValue;
}
}
const addPromiseSupportToDispatch = store => {
const rawDispatch = store.dispatch;
return action => {
if (typeof action.then === "function") {
return action.then(rawDispatch);
}
return rawDispatch(action);
}
}
为了让这两种包装同时运作,我们写一个初始化 store 的函数,以丰富 store.dispatch 的功能:1
2
3
4
5
6
7
8
9
10
11const configureStore = () => {
const store = createStore(App);
if (process.env.NODE_ENV !== "production") {
store.dispatch = addLoggingToDispatch(store);
}
store.dispatch = addPromiseSupportToDispatch(store);
return store;
}
现在,当我们执行 configStore() 后,就获得了一个拥有增强型 dispatch 的 store。仔细研究这个函数,我们发现 addPromiseSupportToDispatch 方法返回了一个符合正常用法的 dispatch,并且支持参数是 Promise。当它内部使用 rawDispatch 进行 action 派发的时候,是最原始的那个 dispatch 吗?显然不是。因为我们已经提前使用 addLoggingToDispatch 对 store.dispatch 进行了修改。
换句话说,当执行到 addPromiseSupportToDispatch 的时候,store.dispatch 是一个已经被包装过的版本。那么我们之前的命名:rawDispatch,就显得极为不合适。那么接下来,我们就给他改名成:next:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const addLoggingToDispatch = store => {
const next = store.dispatch;
if (!console.group) {
return next;
}
return action => {
// ...
const returnValue = next(action);
// ...
return returnValue;
}
}
const addPromiseSupportToDispatch = store => {
const next = store.dispatch;
return action => {
if (typeof action.then === "function") {
return action.then(next);
}
return next(action);
}
}
这样管理太麻烦了。我们可以很自然地想到应该用数组来进行统一管理,即中间件数组。它的每一项就是一个中间件,然后统一根据中间件来增强 dispatch。
说了这么多,那么中间件到底是什么呢?其实就是上面提到的 addLoggingToDispatch、addPromiseSupportToDispatch。
所以,Redux 的核心思想就是将 dispatch 增强改造的函数(中间件)先存起来,然后提供给 Redux,由它负责依次执行。这样,每一个中间件都对 dispatch 依次进行改造,并将改造后的 dispatch,即 next,向下传递,将控制权移交给下一个中间件,完成进一步的增强。具体实现就是:1
2
3
4
5
6
7
8
9
10
11
12
13const configureStore = () => {
const store = cresateStore(App);
const middlewares = [];
if (process.env.NODE_ENV !== "production") {
middlewares.push(addLoggingToDispatch);
}
middlewares.push(addPromiseSupportToDispatch);
wrapDispatchWithMiddlewares(store, middlewares);
return store;
}
如何编写 wrapDispatchWithMiddlewares?1
2
3
4
5const wrapDispatchWithMiddlewares = (store, middlewares) => {
middlewares.forEach(middleware => {
store.dispatch = middleware(store)(store.dispatch);
})
}
与此同时,我们也需要修改中间件。之前设计的是直接读取一个增强后的 dispatch,而在连接各个中间件的时候,需要返回一个返回函数的函数。这样做的意义在于:各个中间件不必再到 store 里面去读取 dispatch,而是将增强的 dispatch 作为参数进行传递和连接,进而层层递进完成控制权的转移:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const promiise = store => next => action => {
if (typeof action.then === "function") {
return action.then(next);
}
return next(action);
}
const logger = store => next => action => {
if (!console.group) {
return next(action);
}
// ...
returnValue = next(action);
// ...
return returnValue;
}
Redux 源码探索——中间件的秘密
源码剖析
Redux 本身暴露了一个 applyMiddleware 的接口,我们只需要引入:1
import { applyMiddleware } from "redux";
同时,对于上面的 2 个例子,有现成的库可以使用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import promise from "redux-promise";
import createLogger from "redux-logger";
const configureStore = () => {
const middlewares = [];
if (process.env.NODE_ENV !== "production") {
middlewares.push(createLogger());
}
middlewares.push(promise);
return createStore(
reducer,
applyMiddleware(...middlewares)
)
}
applyMiddleware 返回的内容我们称之为 enhancer。在 Redux 源码中,涉及中间件的脚本有 applyMiddleware.js、createStore.js、compose.js。那么在 createStore 中,applyMiddleware(...middlewares) 会发生什么事呢?我们找到 createStore.js 的源码部分:1
2
3
4
5
6
7
8
9
10
11export default function createStore(reducer, preloadedState, enhancer) {
// ...
if (typeof enhancer !== "undefined") {
if (typeof enhancer !== "function") {
throw new Error("...");
}
return enhancer(createStore)(reducer, preloadedState);
}
// ...
}
我们再来看看 appliMiddleware 的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
这几行中代码中,应用了大量的函数式编程思想,如 高阶函数、函数组合、柯里化等。下面我们进行拆解。
export default function applyMiddleware(...middlewares)
这里使用了扩展运算符,使得 applyMiddleware 可以接收任意个数的中间件。接下来,它会返回一个函数:
return (createStore) => (reducer, preloadedState) => {...}
对应于 createStore.js 里的代码,它作为一个三级柯里化的函数,相当于:
applyMiddleware(...middlewares)(createStore)(reducer, initialState)
这里借用了原始的 createStore 方法,创建了一个新的增强版 store。
1 | const store = createStore(reducer, preloadedState) |
这里记录了原始的 store 和 dispatch 方法,并准备了一个 chain 数组。
1 | const middlewareAPI = { |
middlewareAPI 是提供给第三方中间件它们需要使用的参数,其中包括了原始的 store.getState 和 dispatch 方法,至于用不用是看它们自己的需求。
dispatch = compose(...chain)(store.dispatch)
最后,通过 compose 方法把各个中间件串联起来。它是怎么实现的呢?1
2
3
4
5
6
7
8
9
10
11export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
事实上,最后的结果就像这个样子:
middlewareA(middlewareB(middlewareC(store.dispatch)))
A -> B -> C -> 最原始的 dispatch
写一个中间件的套路
我们已经了解了中间件的工作原理,它的编写是有固定模式的:1
2
3const customMiddleware = store => next => action => {
// ...
}
我们设想一个场景:应用存在多套主题皮肤可供用户切换选择。这些皮肤在一定时间内往往都是有固定样式的,在初始化整个应用的时候,使用一套默认的主题皮肤。在用户切换主题的情况下,我们希望用户离开应用后,下次再访问时,仍然可以直接切入上一次切换后的主题,而不是默认主题。
切换一套主题的 action 如下:1
2
3
4store.dispatch({
type: "CHANGE_THEME",
payload: "light"
});
那么,我们可以定义一个 CHANGE_THEME 的中间件:1
2
3
4
5
6
7
8
9
10const CHANGE_THEME = store => next => action => {
// 拦截目标 action
if (action.type === "CHANGE_THEME") {
if (localStorage.get("theme") !== action.payload) {
localStorage.setItem("theme", action.payload)
}
}
return next(action);
}
所以每次当用户切换主题的时候,我们通过中间件拦截到了 主题信息,并将其存储到了 localStorage 中。下次访问,我们只需要从 localStorage 里取就可以了。
相关链接
Redux 简单实现(一):状态管理器
Redux 简单实现(二):多文件协作
Redux 简单实现(四):react-redux
参考资料: